<template>
    <div class="big-file-box">
    <div  @click="handleClick">
        <slot></slot>
        <input class="el-upload__input" type="file" ref="input" name="请上传文件" @change='handleChange' :multiple='multiple' :accept='accept'></input> 
    </div>
    <slot name="tip"></slot>
    <div class="file-box">
        <transition-group tag="ul">
            <li v-for="item in uploadFiles"
                :key="item.uid"
                tabindex="0"
                :class="{'file-li':true}"
                @mouseover="handleMouseover(item.uid)"
                @mouseleave="handleMouseleave(item.uid)"
            >
                <i class="el-icon-document"></i>
                <span class="file-name" @click="handleClickFile(item)">{{item.name || item.url}}</span>
                <i v-if="item.status === 'success'" :class="item.uid === listChecked ? 'el-icon-close deleteColor': 'el-icon-circle-check passColor'" @click="hanldeRemoveFile(item)"></i>
                <!-- <i v-else-if="item.status === 'error'" class="el-icon-circle-close"></i>
                <sapn v-if="item.status === 'error'" @click="handleContinueUpload(item)">继续上传</sapn> -->
                <el-progress
                    v-if="item.status === 'uploading'"
                    type="line"
                    :stroke-width="2"
                    :percentage="Number(item.percentage.toFixed(1))">
                </el-progress>
            </li>
        </transition-group>
        </div>
    </div>
    
</template>
<script>
export default {
    name:'BigUpload',
    props:{
        beforeUpload:Function,
        onExceed:Function,
        uploadApi:String,
        mergeApi:String,
        checkApi:String,
        limit: Number,
        accept: String,
        concurrentUpload: {
            type: Boolean,
            default:false
        },
        uploadDisabled:{
            type:Boolean,
            default:false
        },
        fileList: {
            type:Array,
            default:()=>[]
        },
        onRemove: {
            type: Function,
            default: ()=>{}
        },
        onChange: {
            type: Function,
            default: ()=>{}
        },
        onPreview: {
            type: Function
        },
        onSuccess: {
            type: Function,
            default: ()=>{}
        },
        onError: {
            type: Function,
            default: ()=>{}
        },
        onProgress: {
            type: Function,
            default: ()=>{}
        },
        onReadingFile:{
            type:Function,
            default:()=>{}
        },
        chunckSize:{
            type:Number,
            default: 5 * Math.pow(1024,2)
        },
        request:Function,
        apiHeader:Object,
        SparkMD5:Function,
        multiple:{
            type:Boolean,
            default: false
        }
    },
   
    components:{

    },
    data() {
        return {
            promiseArr: [], //请求集合
            tempIndex: 1,
            uploadQuantity: 0,//已上传分片数量
            uploadFiles: [],
            fileSparkMD5:'',
            isUploadChuncks:[], //已上传过的文件分片
            focusing:false,
            listChecked:'', //文件列表悬浮标识
            errorChuncks:[], //预留上传失败的分片
        }
    },
    watch: {
        fileList: {
            immediate: true,
            handler(fileList) {
                this.uploadFiles = fileList.map(item => {
                    item.uid = item.uid || (Date.now() + this.tempIndex++);
                    item.status = item.status || 'success';
                    return item;
                });
            }
        }
    },
    methods:{
        handleClick() {
            this.$refs.input.value = null;
            this.$refs.input.click();
        },

        handleMouseover(uid) {
            this.listChecked = uid;
        },

        handleMouseleave(){
            this.listChecked = '';
        },

        handleClickFile(file) {
            this.onPreview && this.onPreview(file);
        },

        hanldeRemoveFile(file) { 
            let fileList = this.uploadFiles;
            fileList.splice(fileList.indexOf(file), 1);
            this.onRemove && this.onRemove(file,fileList);
        },

        //继续上传
        handleContinueUpload(file) {
            const residueChuncks = this.errorChuncks.map(item => { 
                if(item.hash === file.hash) {
                    return item;
                }
            });
            residueChuncks.forEach(item=>{
                this.post(item.file,item.chunckNumber,this.fileChuncks.length,item.hash);
            })
            this.uploadSuccess();
        },

        async handleChange(ev){
            const files = ev.target.files;
            if (!files) return;
            // if(this.chunckSize > (6*1024*1024)) {
            //     this.uploadFiles1(files);
            // }else {
            //     this.uploadFiles1(files);
            // }
            this.uploadExceedFiles(files);
        },

        uploadExceedFiles(files) {
            if (this.limit && this.fileList.length + files.length > this.limit) {
                //大于限制数量，父组件处理自己的逻辑
                this.onExceed && this.onExceed(files, this.fileList);
                return;
            }
            this.upload(files)
        },

        onStart(rawFile,hash) {
            rawFile.uid = Date.now() + this.tempIndex++;
            let file = {
                status: 'ready',
                name: rawFile.name,
                size: rawFile.size,
                percentage: 0,
                uid: rawFile.uid,
                raw: rawFile,
                hash
            };
            this.uploadFiles.push(file);
            this.onChange(file, this.uploadFiles);
        },

        async upload(files){
            if (!this.beforeUpload) {
                return this.readFile(files[0]);
            }
            const before = this.beforeUpload(files[0]);
            if(before) {
                this.readFile(files[0])
            }
        },

        async readFile(files) {
            this.sliceFile(files);
            this.onReadingFile('start');
            const data = await this.getFileMD5(files);
            this.onReadingFile('end');
            const hasSameFile = this.uploadFiles.findIndex(item=> item.hash ===data);
            if(hasSameFile === -1) {
                this.fileSparkMD5 = {md5Value:data,fileKey:files.name};
                const hasChuncks = await this.checkFile(data,files.name); //是否上传过
                let isSuccess = true; //同步上传成功标记
                if(hasChuncks) {
                    const hasEmptyChunk = this.isUploadChuncks.findIndex(item => item === 0);
                    //上传过,并且已经完整上传，直接提示上传成功（秒传）
                    if(hasEmptyChunk === -1) {
                        let file = {
                            status: 'success',
                            percentage: 100,
                            uid: Date.now() + this.tempIndex++,
                            hash:data,
                            name:1213,
                            url:1213
                        };
                        this.uploadFiles.push(file);
                        this.onSuccess(file);
                        return;
                    }  else {
                        this.onStart(files,data);
                        const emptyLength = this.isUploadChuncks.filter(item => item === 0);
                        for(let k = 0; k < this.isUploadChuncks.length; k++) {
                            if(this.isUploadChuncks[k] !== 1) {
                                let formData = new FormData();
                                formData.append('totalNumber',this.fileChuncks.length);
                                formData.append("chunkSize",this.chunckSize);
                                formData.append("partNumber",k);
                                formData.append('uuid',this.fileSparkMD5.md5Value);
                                formData.append('name',this.fileSparkMD5.fileKey);
                                formData.append('file',new File([this.fileChuncks[k].fileChuncks],this.fileSparkMD5.fileKey))
                                if(this.concurrentUpload) {
                                    this.post(formData,k,emptyLength.length,data); 
                                }else {
                                    isSuccess = await this.post(formData,k,emptyLength.length,data);//这注意分片总数，因为进度条是根据分片个数来算的，所以分母应该是未上传的分片总数
                                    if(!isSuccess) {
                                        break;
                                    }
                                }
                            }
                        }
                        //兼容并发与同步请求操作，受服务器带宽影响，做并发与同步请求处理
                        if(this.concurrentUpload) {
                            this.uploadSuccess();
                        }else {
                            if(isSuccess) {
                                this.mergeFile(this.fileSparkMD5,this.fileChuncks.length);
                            }else {
                                const index = this.uploadFiles.findIndex(item => item.hash === this.fileSparkMD5.md5Value);
                                this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error'}}); //后续拓展继续上传时可用
                                this.hanldeRemoveFile(this.uploadFiles[index]);
                                this.onError(this.uploadFiles[index]);
                            }
                        }
                    }
                }else {
                    this.onStart(files,data);
                    // this.sliceFile(files);
                    //同步上传
                    for(let i = 0; i < this.fileChuncks.length; i++) {
                        let formData = new FormData();
                        formData.append('totalNumber',this.fileChuncks.length);
                        formData.append("chunkSize",this.chunckSize);
                        formData.append("partNumber",i);
                        formData.append('uuid',this.fileSparkMD5.md5Value);
                        formData.append('name',this.fileSparkMD5.fileKey);
                        formData.append('file',new File([this.fileChuncks[i].fileChuncks],this.fileSparkMD5.fileKey));
                        if(this.concurrentUpload) {
                            this.post(formData,k,emptyLength.length,data); 
                        }else {
                            isSuccess = await this.post(formData,i,this.fileChuncks.length,data);//这注意分片总数，因为进度条是根据分片个数来算的，所以分母应该是未上传的分片总数
                            if(!isSuccess) {
                                break;
                            }
                        }
                    }
                    //兼容并发与同步请求操作，受服务器带宽影响，做并发与同步请求处理
                    if(this.concurrentUpload) {
                        this.uploadSuccess();
                    }else {
                        if(isSuccess) {
                            this.mergeFile(this.fileSparkMD5,this.fileChuncks.length);
                        }else {
                            const index = this.uploadFiles.findIndex(item => item.hash === this.fileSparkMD5.md5Value);
                            this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error'}}); //后续拓展继续上传时可用
                            this.hanldeRemoveFile(this.uploadFiles[index]);
                            this.onError(this.uploadFiles[index]);
                        }
                    }
                }
            }else {
                this.$message.error('Please do not upload the same file repeatedly');
            }
        },

        uploadSuccess() {
            Promise.allSettled(this.promiseArr).then(result=>{
                const hasReject = result.findIndex(item => item.status === 'rejected');
                if(hasReject === -1) {
                    this.mergeFile(this.fileSparkMD5,this.fileChuncks.length);
                }else {
                    const index = this.uploadFiles.findIndex(item => item.hash === result[hasReject].reason);
                    this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error'}}); //后续拓展继续上传时可用
                    this.hanldeRemoveFile(this.uploadFiles[index]);
                    this.onError(this.uploadFiles[index]);
                }
            }).catch(e=>{
                this.onError(e);
            }).finally(e=>{
                this.promiseArr = [];
                this.uploadQuantity  = 0; //重置上传进度
            })
        },
        async post(file,nowChunck,totalChunck,hash) {
            let _this = this;
            const index = _this.uploadFiles.findIndex(item => item.hash === hash);
            _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'uploading'}});
            const curPormise = new Promise((resolve,reject)=>{
                let xhr = new XMLHttpRequest();
                // 当上传完成时调用
                xhr.onload = function() {
                    if (xhr.status === 200) {
                        const index = _this.uploadFiles.findIndex(item => item.hash === hash);
                        //大文件上传进度
                        _this.uploadQuantity ++;  //有bug，需要重置后续记得补充逻辑
                        _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'uploading',percentage:_this.uploadQuantity / totalChunck * 100}});
                        _this.onProgress(file,_this.uploadQuantity / totalChunck * 100);
                        resolve(true);
                    }else {
                        _this.errorChuncks.push({file:file,nowChunck,totalChunck,hash});
                        reject(false);
                        _this.uploadQuantity = 0;
                        const index = _this.uploadFiles.findIndex(item => item.hash === hash);
                        _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'error'}}); //后续拓展继续上传时可用
                        _this.hanldeRemoveFile(_this.uploadFiles[index]);
                        _this.onError(_this.uploadFiles[index]);
                    }
                }
                xhr.onerror = function(e) {
                    _this.errorChuncks.push({file:file,nowChunck,totalChunck,hash});
                    reject(false)
                    _this.uploadQuantity = 0;
                    const index = _this.uploadFiles.findIndex(item => item.hash === hash);
                    _this.$set(_this.uploadFiles,index,{..._this.uploadFiles[index],...{status: 'error'}}); //后续拓展继续上传时可用
                    _this.hanldeRemoveFile(_this.uploadFiles[index]);
                    _this.onError(_this.uploadFiles[index]);
                }
                // 发送请求
                xhr.open('POST', _this.uploadApi, true);
                if (_this.apiHeader?.withCredentials && 'withCredentials' in xhr) {
                    xhr.withCredentials = true;
                }
                const headers = _this.apiHeader || {};
                for (let item in headers) {
                    if (headers.hasOwnProperty(item) && headers[item] !== null) {
                        xhr.setRequestHeader(item, headers[item]);
                    }
                }
                xhr.send(file);
            });
            _this.promiseArr.push(curPormise);
            return curPormise;
        },

        sliceFile (file) {
            //文件分片之后的集合
            const chuncks = [];
            let start = 0 ;
            let end;
            while(start < file.size) {
                end = Math.min(start + this.chunckSize,file.size);
                chuncks.push({fileChuncks:file.slice(start,end),fileName:file.name}); 
                start = end;
            }
            //服务端要求最小分片不得小于5M
            // const fileSize = chuncks[chuncks.length-1].fileChuncks;
            // if(fileSize.size < 5 * Math.pow(1024,2)) {
            //     const chunck =  Math.floor(file.size / this.chunckSize) - 1;
            //     const lastStart = this.chunckSize * chunck;
            //     const lastChunck = file.slice(lastStart,file.size);
            //     chuncks.splice(chuncks.length - 2,2,{fileChuncks:lastChunck,fileName:file.name});
            // }
            this.fileChuncks = [...chuncks];
        },

        getFileMD5 (file){
            return new Promise((resolve, reject) => {
                const fileReader = new FileReader();
                fileReader.onload = (e) =>{
                    const fileMd5 = this.SparkMD5.ArrayBuffer.hash(e.target.result);
                    resolve(fileMd5)
                }
                fileReader.onerror = (e) =>{
                    reject('file read failure',e)
                    this.onError(file,'file read failure')
                }
                fileReader.readAsArrayBuffer(file);
            })
        },

        async mergeFile (fileInfo,chunckTotal){
            const { md5Value,fileKey }  = fileInfo;
            const params = {
                totalNumber:chunckTotal,
                md5:md5Value,
                name:fileKey
            }
            const index = this.uploadFiles.findIndex(item => item.hash === md5Value);
            try {
                const {code,data} = await this.request({url:`${this.mergeApi}?uuid=${md5Value}`, method: 'get'});
                if(code === 200) {
                    this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'success',url:data}}); //记得绑定url
                    this.onSuccess(this.uploadFiles[index],data);
                }
            }catch(e) {
                this.$set(this.uploadFiles,index,{...this.uploadFiles[index],...{status: 'error',url:''}}); //记得绑定url
                this.hanldeRemoveFile(this.uploadFiles[index]);
                this.onError(this.uploadFiles[index]);
            }
            this.uploadQuantity = 0;
            this.errorChuncks = [];
        },

        async checkFile(md5Hash,fileName) {
            const {code,data} = await this.request({url:`${this.checkApi}?uuid=${md5Hash}&fileName=${fileName}`, method: 'get'});
            if(code === 200) {
                if(data.length) {
                    const newArr = new Array(Number(this.fileChuncks.length)).fill(0); // [1,1,0,1,1]
                    const chunckNumberArr = data.map(item => item);
                    chunckNumberArr.forEach((item,index) => {
                        newArr[item] = 1
                    });
                    this.isUploadChuncks = [...newArr];
                    return true;
                   
                }else {
                    return false;
                }
            }
        }
    }
}
</script>
<style lang="scss" scoped>
.big-file-box {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;

    .file-li {
        padding: 3px 2px;
        cursor: pointer;
    }

    .file-li:hover {
        background-color: #F2F2F2;
        color:#0075FF ;
    }

    .file-name {
        margin: 0 10px;
    }

    .li-active {
        color: #0075FF;
    }

    .passColor {
        color: #04E81B;
    }

    .deleteColor {
        color: #FD0000;
    }
}
</style>